package org.kirhgoff.mathbot;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.aicontest.Fleet;
import org.aicontest.Planet;
import org.aicontest.PlanetWars;

public class World {
	private static final int POPULATION = 1;
	public static final int OWNER_NEUTRAL = 0;
	public static final int OWNER_ME = 1;
	public static final int OWNER_ENEMY = 2;

	private List<PlanetInfo> planets = new ArrayList<PlanetInfo>();
	private List<FleetInfo> enemyFleets = new ArrayList<FleetInfo>();
	private List<FleetInfo> myFleets = new ArrayList<FleetInfo>();

	private int totalForce = 0;
	
	private int cachedMaxWorldDistance = -1;
	private int maxGrowthRateCache = -1;
	private int overallEnemyForceCache = -1;
	private static Map<PlanetInfo, Map<PlanetInfo, Integer>> distanceCache 
			= new HashMap<PlanetInfo, Map<PlanetInfo, Integer>> ();

	public World(PlanetWars pw, MathModule module) {
		for (Planet planet : pw.Planets()) {
			PlanetInfo planetInfo = new PlanetInfo (planet);
			planets.add (planetInfo);
		}
		
		for (Fleet fleet : pw.EnemyFleets()) {
			enemyFleets.add (new FleetInfo (this, fleet));
		}
		
		for (Fleet fleet : pw.MyFleets()) {
			myFleets.add (new FleetInfo (this, fleet));
		}

		for (PlanetInfo planet : planets) {
			if (planet.isMine()) {
				LOGGER.info("----------------------------------");
				LOGGER.info("WORLD analyzing my planet" + planet);
				//How much ships can the planet send
				int force = planet.population();
				int projection = module.populationAfterLastFleetArrived(this, planet) [POPULATION];
				if (projection < 0) {
					force = 0;
				}
				planet.setForce (force);
				totalForce += planet.force ();
			}
		}
		planets = (List<PlanetInfo>) Collections.unmodifiableList(planets);
		enemyFleets = (List<FleetInfo>) Collections.unmodifiableList(enemyFleets);
	}

	public List<PlanetInfo> getAllPlanets() {
		return planets;
	}

	public int myTotalAttackingForce() {
		return totalForce;
	}
	
	public int getMyPlanetRemainingForce(PlanetInfo myPlanet) {
		return model(myPlanet).force();
	}

	public List<FleetInfo> getTargetingEnemyFleets(PlanetInfo target) {
		return findTargetingFleets(target, enemyFleets);
	}

	public List<FleetInfo> getTargetingMyFleets(PlanetInfo target) {
		return findTargetingFleets(target, myFleets);
	}

	private List<FleetInfo> findTargetingFleets(PlanetInfo target,
			List<FleetInfo> fleets) {
		List<FleetInfo> result = new ArrayList<FleetInfo> ();
		PlanetInfo info = model(target);
		for (FleetInfo fleet : fleets) {
			if (fleet.destinationPlanet () == info) {
				result.add(fleet);
			}
		}
		return result;
	}

	/**
	 * Update world state after this order
	 */
	public void updateWorldAfterThisOrder(PlanetInfo myPlanet, PlanetInfo target, int shipsToSend) {
		int distance = distance(myPlanet, target);
		FleetInfo fleet = new FleetInfo (OWNER_ME, shipsToSend, 
				myPlanet, target, 
					distance, distance);
		
		myFleets.add(fleet);
		myPlanet.setPopulation (myPlanet.population () - shipsToSend);
	}
	
	private PlanetInfo model (PlanetInfo planet) {
		return planet;
	}
	
	public static int distance(PlanetInfo source, PlanetInfo destination) {
		//LOGGER.info("distance source=" + source + " target=" + destination + "\nCache:\n" + distanceCache);
		if (source.equals(destination)) {
			return 0;
		}
		
		Map<PlanetInfo, Integer> distances = distanceCache.get (source);
		if (distances == null) {
			//LOGGER.info ("Distances null");
			distanceCache.put(source, new HashMap<PlanetInfo, Integer> ());
			distanceCache.put(destination, new HashMap<PlanetInfo, Integer> ());
			distances = distanceCache.get (source);
		}
		
		Integer cachedDistance = distances.get(destination);
		if (cachedDistance == null) {
			//LOGGER.info ("Cached distance is null");
			int distance = distanceRaw (source, destination);
			distanceCache.get(source).put(destination, distance);
			
			distances = distanceCache.get(destination);
			if (distances == null) {
				distanceCache.put(destination, new HashMap<PlanetInfo, Integer> ());
				distanceCache.get(destination).put(source, distance);
			}
			cachedDistance = distance;
		}
		
		return cachedDistance.intValue ();
	}

	private static int distanceRaw(PlanetInfo source, PlanetInfo destination) {
		double dx = source.x() - destination.x();
    	double dy = source.y() - destination.y();
    	return (int)Math.ceil(Math.sqrt(dx * dx + dy * dy));
	}

	//TODO optimize
	public PlanetInfo model(int planetID) {
		for (PlanetInfo planet: planets) {
			if (planet.id() == planetID) {
				return planet;
			}
		}
		throw new RuntimeException ("Cannot find planet with id=" + planetID);
	}

	public double maxPopulation() {
		int result = 0;
		for (PlanetInfo planet: planets) {
			if (planet.population() > result) {
				result = planet.population();
			}
		}
		return result;
	}

	public int maxGrowthRate() {
		if (maxGrowthRateCache != -1) {
			return maxGrowthRateCache;
		}
		
		int result = 0;
		for (PlanetInfo planet: planets) {
			if (planet.growthRate() > result) {
				result = planet.growthRate();
			}
		}
		maxGrowthRateCache = result;
		return result;
	}
	
	public int maxWorldDistance(PlanetInfo myPlanet) {
		if (cachedMaxWorldDistance != -1) {
			return cachedMaxWorldDistance;
		}
		int result = 0;
		for (PlanetInfo planet : planets) {
			int distance = distance(myPlanet, planet);
			if (result < distance) {
				result = distance;
			}
		}
		cachedMaxWorldDistance = result;
		return result;
	}

	//TODO optimize
	public int minDistance(PlanetInfo planet, List<PlanetInfo> thePlanets) {
		int result = Integer.MAX_VALUE;
		for (PlanetInfo another : thePlanets) {
			int distance = distance(planet, another);
			if (result > distance) {
				result = distance;
			}
		}
		return result;
	}

	public int getOverallDistance(PlanetInfo planet, int toWhosPlanets) {
		int result = 0;
		for (PlanetInfo anotherPlanet: planets) {
			result += distance (planet, anotherPlanet);
		}
		return result;
	}

	public int planetsCount() {
		return planets.size();
	}

	public int overallEnemyForce() {
		if (overallEnemyForceCache != -1) {
			return overallEnemyForceCache;
		}
		
		int overallEnemyForce = 0;
		for (FleetInfo fleet : enemyFleets) {
			if (fleet.owner() == OWNER_ENEMY) {
				overallEnemyForce += fleet.size();
			} 
		}
		
		for (PlanetInfo planet : planets) {
			if (planet.owner() == OWNER_ENEMY) {
				overallEnemyForce += planet.population();
			} 
		}
		overallEnemyForceCache = overallEnemyForce;
		return overallEnemyForce;
	}

	public int myOverallForce() {
		int myOverallForce = 0;
		for (FleetInfo fleet : myFleets) {
			if (fleet.owner() == OWNER_ME) {
				myOverallForce += fleet.size();
			} 
		}
		
		for (PlanetInfo planet : planets) {
			if (planet.owner() == OWNER_ME) {
				myOverallForce += planet.population();
			} 
		}
		return myOverallForce;
	}

	public List<PlanetInfo> getHighRanked(PlanetInfo planet, final Rank rankForAttackedPlanet) {
		List<PlanetInfo> allPlanets = new ArrayList<PlanetInfo> ();
		allPlanets.addAll(planets);
		Collections.sort(allPlanets, new Comparator<PlanetInfo> () {
			@Override
			public int compare(PlanetInfo one, PlanetInfo two) {
				double rankDiff = rankForAttackedPlanet.rank(World.this, two)
					- rankForAttackedPlanet.rank(World.this, one);
				return (int)(100 * rankDiff); //TODO probably make all calculation int-based?
			}
		});

		return allPlanets;
	}

	public int getEnemyFleetsCount() {
		return enemyFleets.size();
	}

	public int getPlanetsCount() {
		return planets.size();
	}

	public int getMyFleetsCount() {
		return myFleets.size ();
	}
	
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder ();
		for (PlanetInfo planet: planets) {
			sb.append (planet + "\n");
		}
		
		for (FleetInfo fleet : enemyFleets) {
			sb.append (fleet + "\n");
		}
		for (FleetInfo fleet : myFleets) {
			sb.append (fleet + "\n");
		}
		
		return sb.toString();
	}

	public List<PlanetInfo> getMyPlanets() {
		return (List<PlanetInfo>)Collections.unmodifiableList(getPlanetsForOwner (OWNER_ME));
	}

	public List<PlanetInfo> getEnemyPlanets() {
		return (List<PlanetInfo>)Collections.unmodifiableList(getPlanetsForOwner (OWNER_ENEMY));
	}
	
	public void setTotalForce(int totalForce) {
		this.totalForce = totalForce;
	}

	public List<FleetInfo> getAllTargetingFleets(PlanetInfo planet) {
		List<FleetInfo> allFleets = new ArrayList<FleetInfo>();
		allFleets.addAll (getTargetingEnemyFleets(planet));
		allFleets.addAll (getTargetingMyFleets(planet));
		return allFleets;
	}

	public int enemyPlanetsCount() {
		return getPlanetsForOwner (OWNER_ENEMY).size ();
	}

	public List<PlanetInfo> getPlanetsForOwner(int owner) {
		List<PlanetInfo> result = new ArrayList<PlanetInfo> ();
		for (PlanetInfo planet : planets) {
			if (planet.owner() == owner) {
				result.add (planet);
			} 
		}
		return result;
	}

	public static String name (int owner) {
		switch (owner) {
			case OWNER_ENEMY:
				return "enemy";
			case OWNER_ME:
				return "me";
			case OWNER_NEUTRAL:
				return "neutral";
			default:
				throw new IllegalArgumentException("Incorrect owner value owner=" + owner);
		}
	}

	public List<FleetInfo> getAllFleets() {
		List<FleetInfo> fleets = new ArrayList<FleetInfo> ();
		fleets.addAll(myFleets);
		fleets.addAll(enemyFleets);
		return fleets;
	}
}
